package com.xhj.mybatis.scripting.xmltags;

import com.xhj.mybatis.parsing.GenericTokenParser;
import com.xhj.mybatis.session.Configuration;

import java.util.Map;

/**
 * ForEachSqlNode
 *
 * @author XJks
 * @description 用于在 MyBatis 动态 SQL 语句中处理 <foreach> 标签，它会遍历一个集合，并为每个元素生成对应的 SQL 片段。
 */
public class ForEachSqlNode implements SqlNode {
    /**
     * 用于生成唯一参数名的前缀
     */
    public static final String ITEM_PREFIX = "__frch_";

    /**
     * 用于表达式求值的工具类
     */
    private final ExpressionEvaluator evaluator;

    /**
     * 需要遍历的集合表达式
     */
    private final String collectionExpression;

    /**
     * <foreach> 标签内部的 SQL 节点
     */
    private final SqlNode contents;

    /**
     * SQL 片段开头字符串（如 "("）
     */
    private final String open;

    /**
     * SQL 片段结尾字符串（如 ")"）
     */
    private final String close;

    /**
     * 每个元素之间的分隔符（如 ","）
     */
    private final String separator;

    /**
     * 每次遍历时的元素变量名
     */
    private final String item;

    /**
     * 每次遍历时的索引变量名
     */
    private final String index;

    /**
     * MyBatis 配置对象
     */
    private final Configuration configuration;

    /**
     * 构造方法，初始化所有成员变量
     */
    public ForEachSqlNode(Configuration configuration, SqlNode contents, String collectionExpression, String index, String item, String open, String close, String separator) {
        // 初始化表达式求值器
        this.evaluator = new ExpressionEvaluator();
        // 记录集合表达式
        this.collectionExpression = collectionExpression;
        // 记录内部 SQL 节点
        this.contents = contents;
        // 记录开头字符串
        this.open = open;
        // 记录结尾字符串
        this.close = close;
        // 记录分隔符
        this.separator = separator;
        // 记录索引变量名
        this.index = index;
        // 记录元素变量名
        this.item = item;
        // 记录配置对象
        this.configuration = configuration;
    }

    /**
     * 处理 <foreach> 标签，生成对应的 SQL 片段
     */
    @Override
    public boolean apply(DynamicContext context) {
        // 获取当前上下文中的参数绑定
        Map<String, Object> bindings = context.getBindings();
        // 通过表达式求值器获取可遍历的集合
        final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
        // 如果集合为空，直接返回 true
        if (!iterable.iterator().hasNext()) {
            return true;
        }
        // 标记是否为第一个元素
        boolean first = true;
        // 添加开头字符串
        applyOpen(context);
        // 遍历计数
        int i = 0;
        for (Object o : iterable) {
            // 保存原始上下文
            DynamicContext oldContext = context;
            // 第一个元素或没有分隔符时，前缀为空，否则为分隔符
            if (first || separator == null) {
                context = new PrefixedContext(context, "");
            } else {
                context = new PrefixedContext(context, separator);
            }
            // 获取唯一编号，TODO:在循环中 context 都是对传入的 context 进行包装的，该包装类继承了 DynamicContext，所以调用 getUniqueNumber() 方法时，最终调用的还是传入的 context 的 getUniqueNumber() 方法，这样的就能使用同一个计数器来生成唯一编号，保证编号的唯一性。
            int uniqueNumber = context.getUniqueNumber();
            // 如果遍历的是 Map.Entry，则分别处理 key 和 value
            if (o instanceof Map.Entry) {
                // 获取 Map.Entry 对象
                Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
                // 绑定索引变量
                applyIndex(context, mapEntry.getKey(), uniqueNumber);
                // 绑定元素变量
                applyItem(context, mapEntry.getValue(), uniqueNumber);
            } else {
                // 绑定索引变量
                applyIndex(context, i, uniqueNumber);
                // 绑定元素变量
                applyItem(context, o, uniqueNumber);
            }
            /**
             * TODO:
             *  1.绑定完索引变量和元素变量后，创建一个过滤后的动态上下文 FilteredDynamicContext，然后调用 contents.apply 方法，使用子结点组合去处理 SQL 片段。
             *  2.FilteredDynamicContext 会在处理 SQL 片段时，替换掉 SQL 语句中对 item 和 index 变量的引用，改为使用唯一变量名，避免参数名冲突。
             */
            contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
            /**
             * TODO:
             *  疑问:为啥每次在循环末尾都要判断并修改 first 变量？
             *  回答:在 foreach 循环中，使用子结点组合处理 SQL 片段时，可能会涉及到前缀的添加（如逗号、AND 等，如果子结点组合存在 IfSqlNode 节点时，由于某些条件不满足，可能导致某些 SQL 片段没有被添加，从而影响前缀的正确应用，所以需要在每次循环末尾判断并修改 first 变量，以确保前缀的正确应用。
             */
            if (first) {
                first = !((PrefixedContext) context).isPrefixApplied();
            }
            // TODO:恢复原始上下文，以便下一次循环使用
            context = oldContext;
            // 计数递增
            i++;
        }
        // 添加结尾字符串
        applyClose(context);
        // 移除绑定的变量，避免污染后续上下文
        context.getBindings().remove(item);
        context.getBindings().remove(index);
        return true;
    }

    /**
     * 绑定索引变量到上下文
     */
    private void applyIndex(DynamicContext context, Object o, int i) {
        if (index != null) {
            /**
             * TODO:
             *  绑定索引变量。
             *      1.bind 方法的第一个参数 name = index 是一个字符串常量，不会变化。
             *      2.bind 方法的第二个参数 value = o 是一个变量，会随着 foreach 循环的进行而变化。
             *      3.在 foreach 循环的每次迭代中，都会调用 bind 方法，将当前的索引变量绑定到上下文中，覆盖之前绑定的索引变量，该值会随着循环的进行而变化，所以可以通过 index 变量获取到当前的索引变量。
             *      4.综上所述，在 foreach 循环的每次迭代中，由于会先调用 applyIndex 和 applyItem 方法绑定索引变量和元素变量，然后再调用 contents.apply 方法处理内部 SQL 节点，所以在处理内部 SQL 节点时，可以通过 index 变量获取到当前的索引变量对应的元素变量。
             */
            context.bind(index, o);
            // 绑定唯一变量名
            context.bind(itemizeItem(index, i), o);
        }
    }

    /**
     * 绑定元素变量到上下文
     */
    private void applyItem(DynamicContext context, Object o, int i) {
        if (item != null) {
            /**
             * TODO:
             *  绑定元素变量。
             *      1.bind 方法的第一个参数 name = item 是一个字符串常量，不会变化。
             *      2.bind 方法的第二个参数 value = o 是一个变量，会随着 foreach 循环的进行而变化。
             *      3.在 foreach 循环的每次迭代中，都会调用 bind 方法，将当前的元素变量绑定到上下文中，覆盖之前绑定的元素变量，该值会随着循环的进行而变化，所以可以通过 item 变量获取到当前的元素变量。
             *      4.综上所述，在 foreach 循环的每次迭代中，由于会先调用 applyIndex 和 applyItem 方法绑定索引变量和元素变量，然后再调用 contents.apply 方法处理内部 SQL 节点，所以在处理内部 SQL 节点时，可以通过 item 变量获取到当前的索引变量对应的元素变量。
             */
            context.bind(item, o);
            // 绑定唯一变量名
            context.bind(itemizeItem(item, i), o);
        }
    }

    /**
     * 添加开头字符串到 SQL
     */
    private void applyOpen(DynamicContext context) {
        if (open != null) {
            context.appendSql(open);
        }
    }

    /**
     * 添加结尾字符串到 SQL
     */
    private void applyClose(DynamicContext context) {
        if (close != null) {
            context.appendSql(close);
        }
    }

    /**
     * 生成唯一变量名，防止参数名冲突
     */
    private static String itemizeItem(String item, int i) {
        return ITEM_PREFIX + item + "_" + i;
    }

    /**
     * 过滤动态上下文，处理参数名唯一化
     */
    private static class FilteredDynamicContext extends DynamicContext {
        /**
         * 被包装的上下文
         */
        private final DynamicContext delegate;

        /**
         * 当前遍历编号
         */
        private final int index;

        /**
         * 索引变量名
         */
        private final String itemIndex;

        /**
         * 元素变量名
         */
        private final String item;

        public FilteredDynamicContext(Configuration configuration, DynamicContext delegate, String itemIndex, String item, int i) {
            super(configuration, null);
            this.delegate = delegate;
            this.index = i;
            this.itemIndex = itemIndex;
            this.item = item;
        }

        /**
         * 暴露出 bindings，返回一个 Map<String, Object> 视图，外部可以读取已绑定的变量
         *
         * @return 绑定变量的 Map 视图
         */
        @Override
        public Map<String, Object> getBindings() {
            return delegate.getBindings();
        }

        /**
         * 在上下文中绑定一个名字和值
         *
         * @param name  绑定变量的名称
         * @param value 绑定变量的值
         */
        @Override
        public void bind(String name, Object value) {
            delegate.bind(name, value);
        }

        /**
         * 返回当前拼接好的 SQL 字符串，去除首尾多余空白
         *
         * @return 拼接好的 SQL 字符串
         */
        @Override
        public String getSql() {
            return delegate.getSql();
        }

        /**
         * 将一段 SQL 片段追加到 sqlBuilder 中（片段之间用空格隔开）
         *
         * @param sql 要追加的 SQL 片段
         */
        @Override
        public void appendSql(String sql) {
            // 创建一个通用的 Token 解析器，用于处理 SQL 语句中的 #{...} 占位符
            GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> {
                // 将参数名为 item 的部分替换为唯一变量名，避免参数名冲突
                String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index));
                // 如果索引变量 itemIndex 不为空，并且 newContent 没有发生变化，则尝试替换索引变量名为唯一变量名
                if (itemIndex != null && newContent.equals(content)) {
                    newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index));
                }
                // 返回处理后的 #{...} 占位符
                return "#{" + newContent + "}";
            });

            // 使用解析器处理 SQL，将处理后的 SQL 追加到上下文中
            delegate.appendSql(parser.parse(sql));
        }

        /**
         * 返回一个唯一数字（先返回当前值，然后自增），常用于生成占位符或变量名
         *
         * @return 唯一数字
         */
        @Override
        public int getUniqueNumber() {
            return delegate.getUniqueNumber();
        }

    }

    /**
     * 前缀上下文，用于在 SQL 片段前添加分隔符或前缀
     */
    private class PrefixedContext extends DynamicContext {
        /**
         * 被包装的上下文
         */
        private final DynamicContext delegate;

        /**
         * 前缀字符串
         */
        private final String prefix;

        /**
         * 是否已应用前缀
         */
        private boolean prefixApplied;

        public PrefixedContext(DynamicContext delegate, String prefix) {
            super(configuration, null);
            this.delegate = delegate;
            this.prefix = prefix;
            this.prefixApplied = false;
        }

        /**
         * 判断前缀是否已应用
         *
         * @return
         */
        public boolean isPrefixApplied() {
            return prefixApplied;
        }

        /**
         * 暴露出 bindings，返回一个 Map<String, Object> 视图，外部可以读取已绑定的变量
         *
         * @return 绑定变量的 Map 视图
         */
        @Override
        public Map<String, Object> getBindings() {
            return delegate.getBindings();
        }

        /**
         * 在上下文中绑定一个名字和值
         *
         * @param name  绑定变量的名称
         * @param value 绑定变量的值
         */
        @Override
        public void bind(String name, Object value) {
            delegate.bind(name, value);
        }

        /**
         * 将一段 SQL 片段追加到 sqlBuilder 中（片段之间用空格隔开）
         *
         * @param sql 要追加的 SQL 片段
         */
        @Override
        public void appendSql(String sql) {
            // 如果还没有添加前缀，并且传入的 SQL 字符串不为 null 且去除空白后长度大于 0
            if (!prefixApplied && sql != null && sql.trim().length() > 0) {
                // 先将前缀（如逗号、AND 等）追加到 SQL 片段中
                delegate.appendSql(prefix);
                // 标记前缀已经被添加，后续不会重复添加
                prefixApplied = true;
            }
            // 无论是否加了前缀，都要把当前 SQL 片段追加到上下文中
            delegate.appendSql(sql);
        }

        /**
         * 返回当前拼接好的 SQL 字符串，去除首尾多余空白
         *
         * @return 拼接好的 SQL 字符串
         */
        @Override
        public String getSql() {
            return delegate.getSql();
        }

        /**
         * 返回一个唯一数字（先返回当前值，然后自增），常用于生成占位符或变量名
         *
         * @return 唯一数字
         */
        @Override
        public int getUniqueNumber() {
            return delegate.getUniqueNumber();
        }
    }

}
